﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;

using alib;
using alib.Debugging;

namespace agree.itsdb
{
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// Template class for table of Itsb items of some strong-type
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	public abstract class TsdbTable : identity_propchange
	{
		public TsdbTable(TsdbDatabase db, String name)
			: base(db, name)
		{
			this.db = db;
		}

		public object ProvideValue(IServiceProvider sp)
		{
			return this;
		}

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public TsdbDatabase db;

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		protected String db_file;

		//[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		//bool IsLoaded { get; }

		//[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		//int SafeCount { get; }

		//void SetPathAndSchema(String db_file, Schema sch);

		//void ValidateExternalKeys();

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public abstract Type ItsdbType { get; }
		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public abstract int Count { get; }
		public abstract int Add(Object o);
		public abstract void SetPathAndSchema(String db_file, Schema schema);
	};


	[DebuggerDisplay("{ItsdbType.Name.PadRight(22),nq} {Name,nq}")]
	public abstract class TsdbTable<T> : TsdbTable, IList<T>, IReadOnlyList<T>, IBindingList, IIdentList<T>//, IMonad
		where T : entity
	{
		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public Schema<T> schema;

		public TsdbTable(TsdbDatabase db, String name = null)
			: base(db, name)
		{
		}

		[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
		public T[] _items_display { get { return this.ToArray(); } }

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public override Type ItsdbType { get { return typeof(T); } }

		public override void SetPathAndSchema(String db_file, Schema schema)
		{
			base.db_file = Path.Combine(db_file, Name);
			this.schema = (Schema<T>)schema;
		}

		long _cb_uncompressed = -1;
		public long UncompressedFileSize
		{
			get
			{
				if (_cb_uncompressed == -1)
					_get_file_sizes();
				return _cb_uncompressed;
			}
		}
		long _cb_compressed = -1;
		public long CompressedFileSize
		{
			get
			{
				if (_cb_compressed == -1)
					_get_file_sizes();
				return _cb_compressed;
			}
		}

		public void _get_file_sizes()
		{
			try { _cb_uncompressed = new FileInfo(db_file).Length; }
			catch { _cb_uncompressed = 0; }

			base.NotifyPropertyChanged("UncompressedFileSize");

			try { _cb_compressed = new FileInfo(db_file + ".gz").Length; }
			catch { _cb_compressed = 0; }

			base.NotifyPropertyChanged("CompressedFileSize");
		}

		/// <summary>
		/// Using the ordered list of fields specified in 'schema,' load the table with data from a plaintext or gzip 
		/// file with the table's matching name, if any is found in the specified directory.
		/// </summary>
		List<T> _load()
		{
			StreamWriter str_write = null;
			String file_to_use = db_file;
			f_empty = false;
			if (!File.Exists(file_to_use))
			{
				file_to_use += ".gz";
				if (!File.Exists(file_to_use))
				{
					f_empty = true;
					return null;
				}
				if (db.Uncompress)
					str_write = new StreamWriter(db_file, false, Encoding.UTF8);
			}

			List<T> newlist = new List<T>();

			base.Name = typeof(T).Name.Replace('_', '-').ToLower();

			bool f_gave_warning = false;
			using (Stream fstr = File.Open(file_to_use, FileMode.Open, FileAccess.Read, FileShare.Read))
			using (Stream str = file_to_use.EndsWith(".gz") ? (Stream)
											new GZipStream(fstr, CompressionMode.Decompress) :
											new alib.IO.CopyStream(fstr))
			using (StreamReader sr = new StreamReader(str, Encoding.UTF8))
			{
				int i_l = 0;
				String line;
				while ((line = sr.ReadLine()) != null)
				{
					if (str_write != null)
						str_write.WriteLine(line);

					i_l++;
					/// Split the line of raw data into parts
					String[] data = line.Replace(@"\\", @"\").Split('@');
					if (data.Length > schema.Count)
					{
						String msg = String.Format("Number of data items does not match schema in file '{0}', line {1}", file_to_use, i_l);
						throw new Exception(msg);
					}
					else if (!f_gave_warning && data.Length < schema.Count)
					{
						Console.WriteLine("warning: Number of data fields ({0}) is less than the number of fields in the schema ({1}) in file '{2}'", data.Length, schema.Count, file_to_use);
						f_gave_warning = true;
					}

					/// Create an item of the appropriate type and load it with the data from each field
					T o_item = (T)Activator.CreateInstance(typeof(T), this);
					for (int i = 0; i < data.Length; i++)
					{
						FieldInfo fi = schema[i];
						String d = data[i];

						/// Convert the string data into a strongly typed object of the appropriate type
						Object o_data;
						if (fi.FieldType == typeof(long))
						{
							long il;
							if (d == String.Empty)
								il = 0;
							else if (!long.TryParse(d, out il))
							{
								String msg = String.Format("Invalid long integer value '{0}' in file '{1}', line {2}", d, file_to_use, i_l);
								throw new Exception(msg);
							}
							o_data = il;
						}
						else if (fi.FieldType == typeof(int))
						{
							int ii;
							if (d == String.Empty)
								ii = 0;
							else if (!int.TryParse(d, out ii))
							{
								uint ui;
								if (!uint.TryParse(d, out ui))
								{
									String msg = String.Format("Invalid integer value '{0}' in file '{1}', line {2}", d, db_file, i_l);
									throw new Exception(msg);
								}
								ii = (int)ui;
							}
							o_data = ii;
						}
						else if (fi.FieldType == typeof(string))
							o_data = d;
						else if (fi.FieldType == typeof(DateTime))
						{
							String sd = new String(d.Where(ch => ch != '(' && ch != ')').ToArray());
							DateTime dt;
							if (sd == String.Empty)
								dt = default(DateTime);
							else if (!DateTime.TryParse(sd, CultureInfo.GetCultureInfo("de-DE"), DateTimeStyles.None, out dt))
							{
								String msg = String.Format("Unrecognized date/time format '{0}' in file '{1}', line {2}", d, db_file, i_l);
								throw new Exception(msg);
							}
							o_data = dt;
						}
						else
							throw new Exception();

						/// Set the field's value into the item
						fi.SetValue(o_item, o_data);
					}
					/// Add the item to the table
					newlist.Add(o_item);
				}
			}
			if (str_write != null)
			{
				str_write.Close();
				str_write.Dispose();
			}

			/// setup primary keys, if any
			foreach (FieldInfo pkf in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
											.Where(fi => fi.GetCustomAttributes(true).Any(o => o is PrimaryKeyAttribute)))
			{
				if (pkf.FieldType != typeof(int))
					throw new Exception("primary key must be an integer");

				FieldInfo map_field = GetType().GetField(pkf.Name + "_map");
				Dictionary<int, T> map = (Dictionary<int, T>)map_field.GetValue(this);

#if DUPLICATE_KEY_INFO
				Dictionary<int, int> repeated_keys = new Dictionary<int, int>();
#endif
				foreach (T t in newlist)
				{
					int id = (int)pkf.GetValue(t);
					if (map.ContainsKey(id))
					{
#if DUPLICATE_KEY_INFO
						if (repeated_keys.ContainsKey(id))
						    repeated_keys[id]++;
						else
						    repeated_keys.Add(id, 2);
#endif
					}
					else
						map.Add(id, t);
				}
#if DUPLICATE_KEY_INFO
				if (repeated_keys.Count > 0)
				{
				    Console.WriteLine("warning: field '{0}' in table '{1}' has duplicate primary key value(s):", pkf.Name, db_file);
				    foreach (var kvp in repeated_keys.OrderBy(k => k.Key))
				        Console.WriteLine("\tvalue: {0} ({1} instances)", kvp.Key, kvp.Value);
				}
#endif
			}

			//ValidateExternalKeys();
			load_count++;
			return newlist;
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// <summary>
		/// validate external keys via reflection
		/// </summary>
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		public void ValidateExternalKeys()
		{
			/// Process all fields in this table's item type which are marked with the 'ExternalKey' attribute
			foreach (var fa in typeof(T)
								.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
								.SelectMany(fi_this => fi_this.GetCustomAttributes(true)
															.OfType<ExternalKeyAttribute>()
															.Select(attr => new { fi_this, attr })))
			{
				/// Common name of the shared key
				String s_key = fa.fi_this.Name;

				/// type of external item which introduces the primary key
				Type ext_item_type = fa.attr.ti_ext;

				/// find a matching field name in the external item
				FieldInfo fi_ext = ext_item_type.GetField(s_key, BindingFlags.Instance | BindingFlags.NonPublic);
				if (fi_ext == null || fi_ext.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).Length == 0)
				{
					String msg = String.Format("Couldn't resolve primary key '{0}' in table '{1}' referenced by external key in table '{2}'",
						s_key, ext_item_type, Name);
					throw new Exception(msg);
				}

				/// get external table
				TsdbTable ext_table = db.GetTableFromItemType(ext_item_type) as TsdbTable;
				if (ext_table == null)
					continue;

				/// get external table type
				Type ext_table_type = ext_table.GetType();

				/// Get the map out of the external table
				Object map = ext_table_type.GetField(s_key + "_map", BindingFlags.Instance | BindingFlags.NonPublic)
												.GetValue(ext_table);

				/// find the 'ContainsKey' method for a generic dictionary containing the external item as a value
				MethodInfo mi_ContainsKey = typeof(Dictionary<,>)
												.MakeGenericType(new[] { typeof(int), ext_item_type })
												.GetMethod("ContainsKey");

				/// Check each item in this table
				HashSet<int> bad_ids = new HashSet<int>();
				foreach (T t in this)
				{
					/// get the secondary key value specified by an item
					int id = (int)fa.fi_this.GetValue(t);

					/// See if the primary contains the value
					if (!(bool)mi_ContainsKey.Invoke(map, new Object[] { id }))
						bad_ids.Add(id);
				}

				if (bad_ids.Count > 0)
				{
					Console.WriteLine();
					String msg = String.Format("The following value(s) specified for field '{0}' in table '{1}' do not match any value of that field in primary key table '{2}':",
						s_key.Substring(1).Replace('_', '-'),
						Name,
						ext_table.Name);
					Console.WriteLine(msg);
					Console.WriteLine(String.Join(", ", bad_ids.OrderBy(_i => _i)));
				}
			}
		}

		public bool IsLoaded { get { return wr_items.IsAlive; } }

		//public int SafeCount
		//{
		//    get
		//    {
		//        List<T> _tmp = (List<T>)wr_items.Target;
		//        return _tmp == null ? -1 : _tmp.Count;
		//    }
		//}

		int load_count = 0;
		bool f_empty = false;
		WeakReference wr_items = new WeakReference(null);
		IList<T> items
		{
			get
			{
				if (f_empty)
					return alib.Collections.Collection<T>.None;

				List<T> _ret = (List<T>)wr_items.Target;
				if (_ret == null)
				{
					if (load_count > 2)
						Nop.CodeCoverage("tsdb thrashing?");

					List<T> newlist = _load();
					WeakReference _mine = new WeakReference(newlist);
					WeakReference _old;
					while ((_ret = (List<T>)(_old = wr_items).Target) == null)
					{
						WeakReference _cur = Interlocked.CompareExchange(ref wr_items, _mine, _old);
						if (_cur != _old)
							_ret = (List<T>)_cur.Target;
						else
						{
							_ret = newlist;

							base.NotifyPropertyChanged("Count");
							break;
						}
					}
				}
				return _ret;
			}
		}

		//void _fire_list_changed()
		//{
		//    var _tmp = ListChanged;
		//    if (_tmp != null)
		//    {
		//        ListChanged(this, new ListChangedEventArgs(ListChangedType.Reset, 0));
		//    }
		//}

		/// <summary>
		/// List interfaces
		/// </summary>

		public int IndexOf(T item) { return items.IndexOf(item); }
		public void Insert(int index, T item) { items.Insert(index, item); }
		public void RemoveAt(int index) { items.RemoveAt(index); }
		public T this[int index]
		{
			get { return items[index]; }
			set { items[index] = value; }
		}
		public void Add(T item) { items.Add(item); }
		public void Clear() { items.Clear(); }
		public bool Contains(T item) { return items.Contains(item); }
		public void CopyTo(T[] array, int arrayIndex) { items.CopyTo(array, arrayIndex); }
		public bool IsReadOnly { get { return false; } }
		public bool Remove(T item) { return items.Remove(item); }
		public IEnumerator<T> GetEnumerator() { return items.GetEnumerator(); }
		IEnumerator IEnumerable.GetEnumerator() { return items.GetEnumerator(); }

		public bool Contains(object value) { return value is T && items.Contains((T)value); }
		public int IndexOf(object value) { return value is T ? items.IndexOf((T)value) : -1; }
		public void Insert(int index, object value) { items.Insert(index, (T)value); }
		public void Remove(object value) { items.Remove((T)value); }
		object IList.this[int index]
		{
			get { return items[index]; }
			set { items[index] = (T)value; }
		}
		public override int Count { get { return items.Count; } }
		public override int Add(object value)
		{
			if (!(value is T))
				return -1;
			items.Add((T)value);
			return items.Count - 1;
		}

		public void CopyTo(Array array, int index)
		{
			foreach (var item in items)
				array.SetValue(item, index++);
		}

		public event ListChangedEventHandler ListChanged;

		public void AddIndex(PropertyDescriptor property)
		{
			throw not.impl;
		}
		public void RemoveIndex(PropertyDescriptor property)
		{
			throw not.impl;
		}

		public object AddNew() { throw not.impl; }

		public int Find(PropertyDescriptor property, object key)
		{
			throw not.impl;
		}

		public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { throw not.impl; }
		public void RemoveSort() { throw not.impl; }
		public ListSortDirection SortDirection { get { throw not.impl; } }
		public PropertyDescriptor SortProperty { get { throw not.impl; } }

		public bool SupportsChangeNotification { get { throw not.impl; } }
		public bool SupportsSearching { get { throw not.impl; } }
		public bool SupportsSorting { get { throw not.impl; } }
		public bool IsFixedSize { get { throw not.impl; } }
		public bool IsSynchronized { get { throw not.impl; } }
		public bool IsSorted { get { throw not.impl; } }
		public bool AllowEdit { get { throw not.impl; } }
		public bool AllowNew { get { throw not.impl; } }
		public bool AllowRemove { get { throw not.impl; } }

		public object SyncRoot { get { throw not.impl; } }
	};
}
